Skip to content

feat: implement should_apply_proposer_boost for gloas - #9233

Merged
nflaig merged 13 commits into
unstablefrom
nc/should-apply-proposer-boost
Aug 27, 2026
Merged

feat: implement should_apply_proposer_boost for gloas#9233
nflaig merged 13 commits into
unstablefrom
nc/should-apply-proposer-boost

Conversation

@ensi321

@ensi321 ensi321 commented Apr 18, 2026

Copy link
Copy Markdown
Member

Summary

Implements the should_apply_proposer_boost logic ethereum/consensus-specs/pull/4807 (gloas/fork-choice.md#new-should_apply_proposer_boost).

Was blocked on is_head_weak(); now rebased on top of #9654 (merged) and reuses its isHeadWeak() — boost-excluded attestation score + equivocator balance add-back — for the weak-parent check, exactly matching the spec's is_head_weak(store, parent_root) call.

Changes

  • Add ptcTimeliness and proposerIndex fields to ProtoBlock
  • Add isBlockPtcTimely to track PTC deadline timeliness
  • Add shouldApplyProposerBoost which withholds boost when the parent is a weak, equivocating block from the previous slot
  • Reuse isHeadWeak() from feat: implement forkchoice is_head_weak() and is_parent_strong() #9654 for the weak-parent check
  • Add findEquivocatingBlocks in ProtoArray to detect proposer equivocations
  • Gate proposer boost in getWeight on shouldApplyProposerBoost()
  • Pre-gloas blocks retain unconditional boost (backward compatible)

Note on test coverage

The existing gloas fork_choice vectors pass with or without this gate (the withhold branch executes but never decides the asserted head), so CI alone doesn't discriminate it. Discriminating vectors are proposed upstream in consensus-specs (test_should_apply_proposer_boost.py, MC/DC over the three apply conditions); the withheld case fails on unstable and passes on this branch.

AI Assistance Disclosure

Created with the help of Claude.

🤖 Generated with Claude Code

Implement the `should_apply_proposer_boost` logic from consensus-specs
commit 71d1151 (PR #4807). This addresses the builder reveal safety
concern where a colluding next-slot proposer could use proposer boost
to override a legitimately revealed block.

Changes:
- Add `ptcTimeliness` and `proposerIndex` fields to ProtoBlock
- Add `isBlockPtcTimely` to track PTC deadline timeliness
- Add `shouldApplyProposerBoost` which withholds boost when the parent
  is a weak, equivocating block from the previous slot
- Add `findEquivocatingBlocks` in ProtoArray to detect proposer
  equivocations by scanning for PTC-timely blocks at the same slot
  from the same proposer
- Gate proposer boost in `getWeight` on `shouldApplyProposerBoost()`
- Pre-gloas blocks retain unconditional boost (backward compatible)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ensi321
ensi321 requested a review from a team as a code owner April 18, 2026 02:23

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements the Gloas fork-choice logic for proposer boost and PTC (Payload Timeliness Committee) timeliness. It adds ptcTimeliness and proposerIndex to block metadata and introduces the shouldApplyProposerBoost logic, which considers parent block weight and proposer equivocations. Feedback focuses on preventing a crash during the fork transition by using dynamic payload status for parent nodes and optimizing the performance of the equivocation check to avoid O(N) map iterations during head updates.

Comment thread packages/fork-choice/src/forkChoice/forkChoice.ts Outdated
Comment thread packages/fork-choice/src/protoArray/protoArray.ts Outdated
@ensi321
ensi321 marked this pull request as draft April 18, 2026 02:25

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9bc6eb6ce5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/fork-choice/src/forkChoice/forkChoice.ts Outdated
@github-actions

github-actions Bot commented Apr 18, 2026

Copy link
Copy Markdown
Contributor

Performance Report

✔️ no performance regression detected

Full benchmark results
Benchmark suite Current: 774d3e6 Previous: aa8303e Ratio
getPubkeys - native cache - req 1000 vs - 250000 vc 394.74 us/op 416.21 us/op 0.95
getPubkeys - validatorsArr - req 1000 vs - 250000 vc 41.390 us/op 39.601 us/op 1.05
BLS verify - blst 886.48 us/op 895.95 us/op 0.99
BLS verifyMultipleSignatures 3 - blst 1.3674 ms/op 1.3882 ms/op 0.98
BLS verifyMultipleSignatures 8 - blst 2.2161 ms/op 2.2851 ms/op 0.97
BLS verifyMultipleSignatures 32 - blst 7.2863 ms/op 7.5457 ms/op 0.97
BLS verifyMultipleSignatures 64 - blst 14.007 ms/op 14.444 ms/op 0.97
BLS verifyMultipleSignatures 128 - blst 26.885 ms/op 27.024 ms/op 0.99
BLS deserializing 10000 signatures 631.34 ms/op 644.77 ms/op 0.98
BLS deserializing 100000 signatures 6.3721 s/op 6.4772 s/op 0.98
BLS verifyMultipleSignatures - same message - 3 - blst 956.91 us/op 951.72 us/op 1.01
BLS verifyMultipleSignatures - same message - 8 - blst 1.0865 ms/op 1.0771 ms/op 1.01
BLS verifyMultipleSignatures - same message - 32 - blst 1.6959 ms/op 1.6971 ms/op 1.00
BLS verifyMultipleSignatures - same message - 64 - blst 2.5103 ms/op 2.5387 ms/op 0.99
BLS verifyMultipleSignatures - same message - 128 - blst 4.1395 ms/op 4.1707 ms/op 0.99
BLS aggregatePubkeys 32 - blst 19.558 us/op 19.495 us/op 1.00
BLS aggregatePubkeys 128 - blst 70.854 us/op 70.655 us/op 1.00
getSlashingsAndExits - default max 39.167 us/op 39.869 us/op 0.98
getSlashingsAndExits - 2k 335.89 us/op 434.10 us/op 0.77
proposeBlockBody type=full, size=empty 824.52 us/op 1.0188 ms/op 0.81
isKnown best case - 1 super set check 174.00 ns/op 171.00 ns/op 1.02
isKnown normal case - 2 super set checks 176.00 ns/op 166.00 ns/op 1.06
isKnown worse case - 16 super set checks 169.00 ns/op 167.00 ns/op 1.01
validate api signedAggregateAndProof - struct 1.5401 ms/op 1.5582 ms/op 0.99
validate gossip signedAggregateAndProof - struct 1.5355 ms/op 1.5532 ms/op 0.99
batch validate gossip attestation - vc 640000 - chunk 32 127.76 us/op 129.92 us/op 0.98
batch validate gossip attestation - vc 640000 - chunk 64 106.52 us/op 111.01 us/op 0.96
batch validate gossip attestation - vc 640000 - chunk 128 96.685 us/op 100.46 us/op 0.96
batch validate gossip attestation - vc 640000 - chunk 256 95.576 us/op 109.63 us/op 0.87
bytes32 toHexString 292.00 ns/op 291.00 ns/op 1.00
bytes32 Buffer.toString(hex) 170.00 ns/op 179.00 ns/op 0.95
bytes32 Buffer.toString(hex) from Uint8Array 243.00 ns/op 248.00 ns/op 0.98
bytes32 Buffer.toString(hex) + 0x 169.00 ns/op 180.00 ns/op 0.94
Return object 10000 times 0.21010 ns/op 0.21830 ns/op 0.96
Throw Error 10000 times 3.3632 us/op 3.4266 us/op 0.98
toHex 108.03 ns/op 107.66 ns/op 1.00
Buffer.from 93.954 ns/op 90.948 ns/op 1.03
shared Buffer 61.904 ns/op 63.320 ns/op 0.98
fastMsgIdFn sha256 / 200 bytes 1.5080 us/op 1.5040 us/op 1.00
fastMsgIdFn h32 xxhash / 200 bytes 156.00 ns/op 166.00 ns/op 0.94
fastMsgIdFn h64 xxhash / 200 bytes 209.00 ns/op 220.00 ns/op 0.95
fastMsgIdFn sha256 / 1000 bytes 4.8010 us/op 4.8520 us/op 0.99
fastMsgIdFn h32 xxhash / 1000 bytes 246.00 ns/op 256.00 ns/op 0.96
fastMsgIdFn h64 xxhash / 1000 bytes 256.00 ns/op 269.00 ns/op 0.95
fastMsgIdFn sha256 / 10000 bytes 42.739 us/op 42.658 us/op 1.00
fastMsgIdFn h32 xxhash / 10000 bytes 1.2610 us/op 1.4330 us/op 0.88
fastMsgIdFn h64 xxhash / 10000 bytes 823.00 ns/op 926.00 ns/op 0.89
send data - 1000 256B messages 3.8513 ms/op 4.6998 ms/op 0.82
send data - 1000 512B messages 5.0550 ms/op 5.6409 ms/op 0.90
send data - 1000 1024B messages 5.1256 ms/op 5.9187 ms/op 0.87
send data - 1000 1200B messages 5.7771 ms/op 6.9006 ms/op 0.84
send data - 1000 2048B messages 11.001 ms/op 10.090 ms/op 1.09
send data - 1000 4096B messages 81.413 ms/op 113.56 ms/op 0.72
send data - 1000 16384B messages 409.90 ms/op 436.29 ms/op 0.94
send data - 1000 65536B messages 1.9784 s/op 1.8084 s/op 1.09
enrSubnets - fastDeserialize 64 bits 786.00 ns/op 749.00 ns/op 1.05
enrSubnets - ssz BitVector 64 bits 268.00 ns/op 281.00 ns/op 0.95
enrSubnets - fastDeserialize 4 bits 102.00 ns/op 105.00 ns/op 0.97
enrSubnets - ssz BitVector 4 bits 274.00 ns/op 274.00 ns/op 1.00
prioritizePeers score -10:0 att 32-0.1 sync 2-0 209.47 us/op 206.24 us/op 1.02
prioritizePeers score 0:0 att 32-0.25 sync 2-0.25 234.53 us/op 232.23 us/op 1.01
prioritizePeers score 0:0 att 32-0.5 sync 2-0.5 342.28 us/op 333.95 us/op 1.02
prioritizePeers score 0:0 att 64-0.75 sync 4-0.75 609.38 us/op 593.93 us/op 1.03
prioritizePeers score 0:0 att 64-1 sync 4-1 701.57 us/op 688.55 us/op 1.02
array of 16000 items push then shift 1.3294 us/op 1.3125 us/op 1.01
LinkedList of 16000 items push then shift 7.3420 ns/op 6.7940 ns/op 1.08
array of 16000 items push then pop 80.074 ns/op 77.792 ns/op 1.03
LinkedList of 16000 items push then pop 6.0950 ns/op 6.0190 ns/op 1.01
array of 24000 items push then shift 1.9521 us/op 1.9391 us/op 1.01
LinkedList of 24000 items push then shift 6.4590 ns/op 6.3950 ns/op 1.01
array of 24000 items push then pop 114.13 ns/op 110.97 ns/op 1.03
LinkedList of 24000 items push then pop 6.0780 ns/op 5.9830 ns/op 1.02
intersect bitArray bitLen 8 3.8990 ns/op 3.8800 ns/op 1.00
intersect array and set length 8 30.067 ns/op 30.175 ns/op 1.00
intersect bitArray bitLen 128 23.302 ns/op 23.411 ns/op 1.00
intersect array and set length 128 513.93 ns/op 509.93 ns/op 1.01
bitArray.getTrueBitIndexes() bitLen 128 934.00 ns/op 910.00 ns/op 1.03
bitArray.getTrueBitIndexes() bitLen 248 1.7220 us/op 1.6480 us/op 1.04
bitArray.getTrueBitIndexes() bitLen 512 3.5700 us/op 3.4260 us/op 1.04
Full columns - reconstruct all 6 blobs 138.52 us/op 140.34 us/op 0.99
Full columns - reconstruct half of the blobs out of 6 90.197 us/op 80.533 us/op 1.12
Full columns - reconstruct single blob out of 6 35.133 us/op 35.969 us/op 0.98
Half columns - reconstruct all 6 blobs 391.78 ms/op 404.79 ms/op 0.97
Half columns - reconstruct half of the blobs out of 6 194.68 ms/op 204.96 ms/op 0.95
Half columns - reconstruct single blob out of 6 69.466 ms/op 67.589 ms/op 1.03
Set add up to 64 items then delete first 1.7005 us/op 1.6770 us/op 1.01
OrderedSet add up to 64 items then delete first 2.5698 us/op 2.5268 us/op 1.02
Set add up to 64 items then delete last 1.9116 us/op 1.9007 us/op 1.01
OrderedSet add up to 64 items then delete last 2.8255 us/op 2.9110 us/op 0.97
Set add up to 64 items then delete middle 1.9289 us/op 1.9262 us/op 1.00
OrderedSet add up to 64 items then delete middle 4.2859 us/op 4.4581 us/op 0.96
Set add up to 128 items then delete first 3.8409 us/op 3.7244 us/op 1.03
OrderedSet add up to 128 items then delete first 5.8864 us/op 5.6392 us/op 1.04
Set add up to 128 items then delete last 3.7099 us/op 3.6852 us/op 1.01
OrderedSet add up to 128 items then delete last 5.4358 us/op 5.6575 us/op 0.96
Set add up to 128 items then delete middle 3.6906 us/op 3.7302 us/op 0.99
OrderedSet add up to 128 items then delete middle 11.329 us/op 11.821 us/op 0.96
Set add up to 256 items then delete first 7.7532 us/op 7.3851 us/op 1.05
OrderedSet add up to 256 items then delete first 11.885 us/op 11.463 us/op 1.04
Set add up to 256 items then delete last 7.3177 us/op 7.2755 us/op 1.01
OrderedSet add up to 256 items then delete last 11.249 us/op 11.690 us/op 0.96
Set add up to 256 items then delete middle 7.4885 us/op 7.5312 us/op 0.99
OrderedSet add up to 256 items then delete middle 34.991 us/op 35.598 us/op 0.98
runFastConfirmationRules vc:100000 bc:96 eq:0 5.0068 ms/op 4.8055 ms/op 1.04
runFastConfirmationRules vc:600000 bc:96 eq:0 35.561 ms/op 34.765 ms/op 1.02
runFastConfirmationRules vc:1000000 bc:96 eq:0 59.126 ms/op 57.337 ms/op 1.03
runFastConfirmationRules vc:600000 bc:320 eq:0 35.245 ms/op 34.343 ms/op 1.03
runFastConfirmationRules vc:100000 bc:96 eq:1000 1.1394 s/op 1.1561 s/op 0.99
pass gossip attestations to forkchoice per slot 2.5959 ms/op 2.5339 ms/op 1.02
forkChoice updateHead vc 100000 bc 64 eq 0 453.16 us/op 455.69 us/op 0.99
forkChoice updateHead vc 600000 bc 64 eq 0 2.8408 ms/op 2.6765 ms/op 1.06
forkChoice updateHead vc 1000000 bc 64 eq 0 4.6066 ms/op 4.5720 ms/op 1.01
forkChoice updateHead vc 600000 bc 320 eq 0 2.8565 ms/op 2.7676 ms/op 1.03
forkChoice updateHead vc 600000 bc 1200 eq 0 2.8645 ms/op 2.7871 ms/op 1.03
forkChoice updateHead vc 600000 bc 7200 eq 0 3.3265 ms/op 3.2453 ms/op 1.03
forkChoice updateHead vc 600000 bc 64 eq 1000 2.8483 ms/op 2.8302 ms/op 1.01
forkChoice updateHead vc 600000 bc 64 eq 10000 2.9622 ms/op 2.9190 ms/op 1.01
forkChoice updateHead vc 600000 bc 64 eq 300000 7.3908 ms/op 7.1275 ms/op 1.04
forkChoice updateHead vc 600000 bc 64 eq 0 gloas boosted 2.7988 ms/op
computeDeltas 1400000 validators 0% inactive 13.425 ms/op 13.745 ms/op 0.98
computeDeltas 1400000 validators 10% inactive 12.666 ms/op 12.568 ms/op 1.01
computeDeltas 1400000 validators 20% inactive 11.943 ms/op 11.826 ms/op 1.01
computeDeltas 1400000 validators 50% inactive 9.5161 ms/op 9.6475 ms/op 0.99
computeDeltas 2100000 validators 0% inactive 19.437 ms/op 19.830 ms/op 0.98
computeDeltas 2100000 validators 10% inactive 18.551 ms/op 18.885 ms/op 0.98
computeDeltas 2100000 validators 20% inactive 17.994 ms/op 17.784 ms/op 1.01
computeDeltas 2100000 validators 50% inactive 14.484 ms/op 14.476 ms/op 1.00
altair processAttestation - 250000 vs - 7PWei normalcase 1.6361 ms/op 1.5935 ms/op 1.03
altair processAttestation - 250000 vs - 7PWei worstcase 2.4452 ms/op 2.3845 ms/op 1.03
altair processAttestation - setStatus - 1/6 committees join 105.37 us/op 107.14 us/op 0.98
altair processAttestation - setStatus - 1/3 committees join 204.51 us/op 200.48 us/op 1.02
altair processAttestation - setStatus - 1/2 committees join 298.63 us/op 291.31 us/op 1.03
altair processAttestation - setStatus - 2/3 committees join 381.11 us/op 384.38 us/op 0.99
altair processAttestation - setStatus - 4/5 committees join 536.68 us/op 530.98 us/op 1.01
altair processAttestation - setStatus - 100% committees join 625.19 us/op 622.96 us/op 1.00
altair processBlock - 250000 vs - 7PWei normalcase 3.5072 ms/op 3.3855 ms/op 1.04
altair processBlock - 250000 vs - 7PWei normalcase hashState 20.030 ms/op 19.145 ms/op 1.05
altair processBlock - 250000 vs - 7PWei worstcase 23.301 ms/op 22.650 ms/op 1.03
altair processBlock - 250000 vs - 7PWei worstcase hashState 52.247 ms/op 49.072 ms/op 1.06
phase0 processBlock - 250000 vs - 7PWei normalcase 1.3381 ms/op 1.2722 ms/op 1.05
phase0 processBlock - 250000 vs - 7PWei worstcase 19.783 ms/op 19.429 ms/op 1.02
altair processEth1Data - 250000 vs - 7PWei normalcase 312.30 us/op 299.68 us/op 1.04
getExpectedWithdrawals 250000 eb:1,eth1:1,we:0,wn:0,smpl:16 4.1440 us/op 3.5750 us/op 1.16
getExpectedWithdrawals 250000 eb:0.95,eth1:0.1,we:0.05,wn:0,smpl:220 22.207 us/op 21.803 us/op 1.02
getExpectedWithdrawals 250000 eb:0.95,eth1:0.3,we:0.05,wn:0,smpl:43 6.0800 us/op 5.9780 us/op 1.02
getExpectedWithdrawals 250000 eb:0.95,eth1:0.7,we:0.05,wn:0,smpl:19 3.7200 us/op 3.8610 us/op 0.96
getExpectedWithdrawals 250000 eb:0.1,eth1:0.1,we:0,wn:0,smpl:1021 92.652 us/op 100.21 us/op 0.92
getExpectedWithdrawals 250000 eb:0.03,eth1:0.03,we:0,wn:0,smpl:11778 1.4681 ms/op 1.4383 ms/op 1.02
getExpectedWithdrawals 250000 eb:0.01,eth1:0.01,we:0,wn:0,smpl:16384 1.9162 ms/op 1.8691 ms/op 1.03
getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,smpl:16384 1.8772 ms/op 1.8636 ms/op 1.01
getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,nocache,smpl:16384 3.4035 ms/op 3.7627 ms/op 0.90
getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,smpl:16384 2.0855 ms/op 2.1229 ms/op 0.98
getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,nocache,smpl:16384 3.8007 ms/op 3.9687 ms/op 0.96
Tree 40 250000 create 323.24 ms/op 316.81 ms/op 1.02
Tree 40 250000 get(125000) 100.36 ns/op 99.713 ns/op 1.01
Tree 40 250000 set(125000) 1.0747 us/op 1.0784 us/op 1.00
Tree 40 250000 toArray() 9.8812 ms/op 9.9920 ms/op 0.99
Tree 40 250000 iterate all - toArray() + loop 10.229 ms/op 10.193 ms/op 1.00
Tree 40 250000 iterate all - get(i) 33.696 ms/op 33.802 ms/op 1.00
Array 250000 create 2.4199 ms/op 2.3925 ms/op 1.01
Array 250000 clone - spread 760.05 us/op 754.90 us/op 1.01
Array 250000 get(125000) 0.29400 ns/op 0.29600 ns/op 0.99
Array 250000 set(125000) 0.29500 ns/op 0.29600 ns/op 1.00
Array 250000 iterate all - loop 56.101 us/op 56.886 us/op 0.99
phase0 afterProcessEpoch - 250000 vs - 7PWei 39.922 ms/op 38.144 ms/op 1.05
Array.fill - length 1000000 4.1917 ms/op 4.1075 ms/op 1.02
Array push - length 1000000 13.680 ms/op 14.240 ms/op 0.96
Array.get 0.20278 ns/op 0.20275 ns/op 1.00
Uint8Array.get 0.25082 ns/op 0.25634 ns/op 0.98
phase0 beforeProcessEpoch - 250000 vs - 7PWei 14.288 ms/op 13.200 ms/op 1.08
altair processEpoch - mainnet_e81889 207.86 ms/op 227.02 ms/op 0.92
mainnet_e81889 - altair beforeProcessEpoch 41.825 ms/op 51.411 ms/op 0.81
mainnet_e81889 - altair processJustificationAndFinalization 4.3590 us/op 4.5400 us/op 0.96
mainnet_e81889 - altair processInactivityUpdates 3.5273 ms/op 3.6607 ms/op 0.96
mainnet_e81889 - altair processRewardsAndPenalties 16.730 ms/op 19.705 ms/op 0.85
mainnet_e81889 - altair processRegistryUpdates 548.00 ns/op 575.00 ns/op 0.95
mainnet_e81889 - altair processSlashings 136.00 ns/op 142.00 ns/op 0.96
mainnet_e81889 - altair processEth1DataReset 134.00 ns/op 142.00 ns/op 0.94
mainnet_e81889 - altair processEffectiveBalanceUpdates 1.3003 ms/op 1.7660 ms/op 0.74
mainnet_e81889 - altair processSlashingsReset 692.00 ns/op 727.00 ns/op 0.95
mainnet_e81889 - altair processRandaoMixesReset 938.00 ns/op 970.00 ns/op 0.97
mainnet_e81889 - altair processHistoricalRootsUpdate 135.00 ns/op 144.00 ns/op 0.94
mainnet_e81889 - altair processParticipationFlagUpdates 436.00 ns/op 488.00 ns/op 0.89
mainnet_e81889 - altair processSyncCommitteeUpdates 112.00 ns/op 113.00 ns/op 0.99
mainnet_e81889 - altair afterProcessEpoch 41.170 ms/op 41.481 ms/op 0.99
capella processEpoch - mainnet_e217614 696.41 ms/op 610.41 ms/op 1.14
mainnet_e217614 - capella beforeProcessEpoch 64.759 ms/op 70.050 ms/op 0.92
mainnet_e217614 - capella processJustificationAndFinalization 4.3940 us/op 4.3920 us/op 1.00
mainnet_e217614 - capella processInactivityUpdates 11.812 ms/op 11.668 ms/op 1.01
mainnet_e217614 - capella processRewardsAndPenalties 91.074 ms/op 92.421 ms/op 0.99
mainnet_e217614 - capella processRegistryUpdates 4.5920 us/op 4.5130 us/op 1.02
mainnet_e217614 - capella processSlashings 141.00 ns/op 143.00 ns/op 0.99
mainnet_e217614 - capella processEth1DataReset 136.00 ns/op 154.00 ns/op 0.88
mainnet_e217614 - capella processEffectiveBalanceUpdates 6.3931 ms/op 5.5934 ms/op 1.14
mainnet_e217614 - capella processSlashingsReset 683.00 ns/op 694.00 ns/op 0.98
mainnet_e217614 - capella processRandaoMixesReset 928.00 ns/op 922.00 ns/op 1.01
mainnet_e217614 - capella processHistoricalRootsUpdate 137.00 ns/op 139.00 ns/op 0.99
mainnet_e217614 - capella processParticipationFlagUpdates 442.00 ns/op 443.00 ns/op 1.00
mainnet_e217614 - capella afterProcessEpoch 107.30 ms/op 106.03 ms/op 1.01
phase0 processEpoch - mainnet_e58758 200.84 ms/op 204.33 ms/op 0.98
mainnet_e58758 - phase0 beforeProcessEpoch 42.438 ms/op 39.937 ms/op 1.06
mainnet_e58758 - phase0 processJustificationAndFinalization 4.5400 us/op 4.4750 us/op 1.01
mainnet_e58758 - phase0 processRewardsAndPenalties 15.734 ms/op 16.041 ms/op 0.98
mainnet_e58758 - phase0 processRegistryUpdates 2.2650 us/op 2.2810 us/op 0.99
mainnet_e58758 - phase0 processSlashings 137.00 ns/op 142.00 ns/op 0.96
mainnet_e58758 - phase0 processEth1DataReset 134.00 ns/op 137.00 ns/op 0.98
mainnet_e58758 - phase0 processEffectiveBalanceUpdates 927.06 us/op 870.22 us/op 1.07
mainnet_e58758 - phase0 processSlashingsReset 801.00 ns/op 831.00 ns/op 0.96
mainnet_e58758 - phase0 processRandaoMixesReset 941.00 ns/op 1000.0 ns/op 0.94
mainnet_e58758 - phase0 processHistoricalRootsUpdate 136.00 ns/op 214.00 ns/op 0.64
mainnet_e58758 - phase0 processParticipationRecordUpdates 917.00 ns/op 992.00 ns/op 0.92
mainnet_e58758 - phase0 afterProcessEpoch 32.625 ms/op 33.730 ms/op 0.97
phase0 processEffectiveBalanceUpdates - 250000 normalcase 1.0109 ms/op 1.0197 ms/op 0.99
phase0 processEffectiveBalanceUpdates - 250000 worstcase 0.5 1.2308 ms/op 1.6387 ms/op 0.75
altair processInactivityUpdates - 250000 normalcase 10.728 ms/op 10.854 ms/op 0.99
altair processInactivityUpdates - 250000 worstcase 10.794 ms/op 10.983 ms/op 0.98
phase0 processRegistryUpdates - 250000 normalcase 2.2980 us/op 2.2810 us/op 1.01
phase0 processRegistryUpdates - 250000 badcase_full_deposits 146.56 us/op 148.59 us/op 0.99
phase0 processRegistryUpdates - 250000 worstcase 0.5 60.425 ms/op 62.248 ms/op 0.97
altair processRewardsAndPenalties - 250000 normalcase 13.597 ms/op 13.653 ms/op 1.00
altair processRewardsAndPenalties - 250000 worstcase 13.185 ms/op 13.359 ms/op 0.99
phase0 getAttestationDeltas - 250000 normalcase 5.5439 ms/op 5.5854 ms/op 0.99
phase0 getAttestationDeltas - 250000 worstcase 5.5887 ms/op 5.5902 ms/op 1.00
phase0 processSlashings - 250000 worstcase 61.587 us/op 59.866 us/op 1.03
altair processSyncCommitteeUpdates - 250000 9.8906 ms/op 9.8426 ms/op 1.00
BeaconState.hashTreeRoot - No change 168.00 ns/op 175.00 ns/op 0.96
BeaconState.hashTreeRoot - 1 full validator 64.254 us/op 63.125 us/op 1.02
BeaconState.hashTreeRoot - 32 full validator 695.85 us/op 630.55 us/op 1.10
BeaconState.hashTreeRoot - 512 full validator 7.1500 ms/op 6.4314 ms/op 1.11
BeaconState.hashTreeRoot - 1 validator.effectiveBalance 78.651 us/op 71.680 us/op 1.10
BeaconState.hashTreeRoot - 32 validator.effectiveBalance 1.1390 ms/op 1.1116 ms/op 1.02
BeaconState.hashTreeRoot - 512 validator.effectiveBalance 15.000 ms/op 13.772 ms/op 1.09
BeaconState.hashTreeRoot - 1 balances 62.783 us/op 59.117 us/op 1.06
BeaconState.hashTreeRoot - 32 balances 611.05 us/op 587.43 us/op 1.04
BeaconState.hashTreeRoot - 512 balances 5.6935 ms/op 4.5947 ms/op 1.24
BeaconState.hashTreeRoot - 250000 balances 114.86 ms/op 106.50 ms/op 1.08
aggregationBits - 2048 els - zipIndexesInBitList 19.665 us/op 19.519 us/op 1.01
regular array get 100000 times 22.509 us/op 22.727 us/op 0.99
wrappedArray get 100000 times 22.346 us/op 22.805 us/op 0.98
arrayWithProxy get 100000 times 10.128 ms/op 10.316 ms/op 0.98
ssz.Root.equals 21.191 ns/op 21.472 ns/op 0.99
byteArrayEquals 20.965 ns/op 21.258 ns/op 0.99
Buffer.compare 9.3360 ns/op 9.0160 ns/op 1.04
processSlot - 1 slots 8.7300 us/op 8.1600 us/op 1.07
processSlot - 32 slots 1.5920 ms/op 1.5776 ms/op 1.01
getEffectiveBalanceIncrementsZeroInactive - 250000 vs - 7PWei 3.6526 ms/op 3.1702 ms/op 1.15
getCommitteeAssignments - req 1 vs - 250000 vc 1.6631 ms/op 1.6745 ms/op 0.99
getCommitteeAssignments - req 100 vs - 250000 vc 3.4154 ms/op 3.3962 ms/op 1.01
getCommitteeAssignments - req 1000 vs - 250000 vc 3.6694 ms/op 3.6431 ms/op 1.01
findModifiedValidators - 10000 modified validators 818.20 ms/op 759.44 ms/op 1.08
findModifiedValidators - 1000 modified validators 543.59 ms/op 629.86 ms/op 0.86
findModifiedValidators - 100 modified validators 415.66 ms/op 358.88 ms/op 1.16
findModifiedValidators - 10 modified validators 302.66 ms/op 325.66 ms/op 0.93
findModifiedValidators - 1 modified validators 255.44 ms/op 222.11 ms/op 1.15
findModifiedValidators - no difference 274.40 ms/op 201.08 ms/op 1.36
migrate state 1500000 validators, 3400 modified, 2000 new 3.7752 s/op 2.7279 s/op 1.38
RootCache.getBlockRootAtSlot - 250000 vs - 7PWei 3.8200 ns/op 3.6600 ns/op 1.04
state getBlockRootAtSlot - 250000 vs - 7PWei 454.23 ns/op 289.41 ns/op 1.57
computeProposerIndex 100000 validators 1.5858 ms/op 1.3686 ms/op 1.16
getNextSyncCommitteeIndices 1000 validators 3.2277 ms/op 2.8786 ms/op 1.12
getNextSyncCommitteeIndices 10000 validators 28.194 ms/op 26.020 ms/op 1.08
getNextSyncCommitteeIndices 100000 validators 98.724 ms/op 88.925 ms/op 1.11
computeProposers - vc 250000 637.04 us/op 549.05 us/op 1.16
computeEpochShuffling - vc 250000 41.417 ms/op 38.481 ms/op 1.08
getNextSyncCommittee - vc 250000 10.315 ms/op 9.4446 ms/op 1.09
nodejs block root to RootHex using toHex 102.14 ns/op 103.01 ns/op 0.99
nodejs block root to RootHex using toRootHex 66.306 ns/op 68.148 ns/op 0.97
nodejs fromHex(blob) 1.3168 ms/op 899.28 us/op 1.46
nodejs fromHexInto(blob) 657.88 us/op 663.52 us/op 0.99
nodejs block root to RootHex using the deprecated toHexString 610.01 ns/op 610.14 ns/op 1.00
nodejs byteArrayEquals 32 bytes (block root) 26.762 ns/op 25.884 ns/op 1.03
nodejs byteArrayEquals 48 bytes (pubkey) 38.757 ns/op 37.387 ns/op 1.04
nodejs byteArrayEquals 96 bytes (signature) 38.225 ns/op 40.169 ns/op 0.95
nodejs byteArrayEquals 1024 bytes 45.753 ns/op 45.096 ns/op 1.01
nodejs byteArrayEquals 131072 bytes (blob) 1.8226 us/op 1.7490 us/op 1.04
browser block root to RootHex using toHex 150.05 ns/op 146.59 ns/op 1.02
browser block root to RootHex using toRootHex 134.05 ns/op 131.68 ns/op 1.02
browser fromHex(blob) 1.8792 ms/op 1.7361 ms/op 1.08
browser fromHexInto(blob) 664.16 us/op 667.80 us/op 0.99
browser block root to RootHex using the deprecated toHexString 403.88 ns/op 398.98 ns/op 1.01
browser byteArrayEquals 32 bytes (block root) 28.482 ns/op 27.936 ns/op 1.02
browser byteArrayEquals 48 bytes (pubkey) 40.269 ns/op 39.423 ns/op 1.02
browser byteArrayEquals 96 bytes (signature) 75.677 ns/op 73.812 ns/op 1.03
browser byteArrayEquals 1024 bytes 766.06 ns/op 751.12 ns/op 1.02
browser byteArrayEquals 131072 bytes (blob) 96.674 us/op 94.628 us/op 1.02

by benchmarkbot/action

GrapeBaBa added a commit to GrapeBaBa/lodestar that referenced this pull request May 1, 2026
Wire the consensus-specs Fork Choice Compliance suite (ChainSafe#3831) into
the existing `forkChoiceTest` runner. The on-disk layout matches the
standard spec-test layout
(`tests/<preset>/<fork>/fork_choice_compliance/<handler>/<suite>/<case>/`),
so it slots in alongside `fork_choice` and `sync` runners.

Three test-only accommodations the compliance fixtures require:

1. `bls_setting: 2` — every compliance fixture uses placeholder
   signatures. Pass `validSignatures: testcase.meta?.bls_setting !==
   BigInt(1)` to `chain.processBlock` so verification short-circuits.
   Standard `fork_choice` fixtures use `bls_setting: 1` so behavior
   there is unchanged.

2. `BLOCK_ERROR_ALREADY_KNOWN` — compliance fixtures intentionally
   re-import the same block (`dup_shift` mutations in their
   `meta.yaml`). Spec semantics for `on_block(store, known_block)` is
   a no-op success. Production block import correctly rejects with
   ALREADY_KNOWN; this runner treats that case as success only when
   the step is `valid: true`.

3. Cross-epoch attestation shuffling — `on_attestation` decodes
   aggregation_bits using the state at the attestation's target
   checkpoint, not the head state. The runner now resolves the right
   shuffling via ShufflingCache + regen (mirroring the production
   validation path) instead of `headState.epochCtx.getIndexedAttestation`,
   which only worked when the attestation's epoch happened to be in
   the head's epoch cache (±1 epoch) and broke on cross-epoch fork
   attestations surfaced by the compliance suite.

Adds support for two compliance-only check fields:

- `viable_for_head_roots_and_weights` (consensus-specs#3831): compared
  via `getViableHeads()`. Both sides are sorted by root before
  comparison since the spec doesn't fix order.
- `head_payload_status` (gloas): mapped between our internal enum
  ordering (PENDING=0, EMPTY=1, FULL=2) and spec ordering (EMPTY=0,
  FULL=1, PENDING=2).

Pass rate against the latest comptests workflow `small.tar.gz` artifact:

  fulu/fork_choice_compliance: 253/1472 cases pass (17.2%)

Top remaining failures:
- ~80% `Invalid proposer boost root` — consensus-specs#4807 introduced
  a `block.proposer_index == get_beacon_proposer_index(head_state)`
  guard in `update_proposer_boost_root` that we do not yet implement;
  affects all forks (not just gloas equivocation handling). Tracked
  for follow-up alongside ChainSafe#9233.
- ~1% `Invalid viable heads` — proposer-boost rounding on minimal
  preset (see `getViableHeads()` weight note).
GrapeBaBa added a commit to GrapeBaBa/lodestar that referenced this pull request May 1, 2026
Wire the consensus-specs Fork Choice Compliance suite (ChainSafe#3831) into
the existing `forkChoiceTest` runner. The on-disk layout matches the
standard spec-test layout
(`tests/<preset>/<fork>/fork_choice_compliance/<handler>/<suite>/<case>/`),
so it slots in alongside `fork_choice` and `sync` runners.

Three test-only accommodations the compliance fixtures require:

1. `bls_setting: 2` — every compliance fixture uses placeholder
   signatures. Pass `validSignatures: testcase.meta?.bls_setting !==
   BigInt(1)` to `chain.processBlock` so verification short-circuits.
   Standard `fork_choice` fixtures use `bls_setting: 1` so behavior
   there is unchanged.

2. `BLOCK_ERROR_ALREADY_KNOWN` — compliance fixtures intentionally
   re-import the same block (`dup_shift` mutations in their
   `meta.yaml`). Spec semantics for `on_block(store, known_block)` is
   a no-op success. Production block import correctly rejects with
   ALREADY_KNOWN; this runner treats that case as success only when
   the step is `valid: true`.

3. Cross-epoch attestation shuffling — `on_attestation` decodes
   aggregation_bits using the state at the attestation's target
   checkpoint, not the head state. The runner now resolves the right
   shuffling via ShufflingCache + regen (mirroring the production
   validation path) instead of `headState.epochCtx.getIndexedAttestation`,
   which only worked when the attestation's epoch happened to be in
   the head's epoch cache (±1 epoch) and broke on cross-epoch fork
   attestations surfaced by the compliance suite.

Adds support for two compliance-only check fields:

- `viable_for_head_roots_and_weights` (consensus-specs#3831): compared
  via `getViableHeads()`. Both sides are sorted by root before
  comparison since the spec doesn't fix order.
- `head_payload_status` (gloas): mapped between our internal enum
  ordering (PENDING=0, EMPTY=1, FULL=2) and spec ordering (EMPTY=0,
  FULL=1, PENDING=2).

Pass rate against the latest comptests workflow `small.tar.gz` artifact:

  fulu/fork_choice_compliance: 253/1472 cases pass (17.2%)

Top remaining failures:
- ~80% `Invalid proposer boost root` — consensus-specs#4807 introduced
  a `block.proposer_index == get_beacon_proposer_index(head_state)`
  guard in `update_proposer_boost_root` that we do not yet implement;
  affects all forks (not just gloas equivocation handling). Tracked
  for follow-up alongside ChainSafe#9233.
- ~1% `Invalid viable heads` — proposer-boost rounding on minimal
  preset (see `getViableHeads()` weight note).
GrapeBaBa added a commit to GrapeBaBa/lodestar that referenced this pull request May 1, 2026
Wire the consensus-specs Fork Choice Compliance suite (ChainSafe#3831) into
the existing `forkChoiceTest` runner. The on-disk layout matches the
standard spec-test layout
(`tests/<preset>/<fork>/fork_choice_compliance/<handler>/<suite>/<case>/`),
so it slots in alongside `fork_choice` and `sync` runners.

Three test-only accommodations the compliance fixtures require:

1. `bls_setting: 2` — every compliance fixture uses placeholder
   signatures. Pass `validSignatures: testcase.meta?.bls_setting !==
   BigInt(1)` to `chain.processBlock` so verification short-circuits.
   Standard `fork_choice` fixtures use `bls_setting: 1` so behavior
   there is unchanged.

2. `BLOCK_ERROR_ALREADY_KNOWN` — compliance fixtures intentionally
   re-import the same block (`dup_shift` mutations in their
   `meta.yaml`). Spec semantics for `on_block(store, known_block)` is
   a no-op success. Production block import correctly rejects with
   ALREADY_KNOWN; this runner treats that case as success only when
   the step is `valid: true`.

3. Cross-epoch attestation shuffling — `on_attestation` decodes
   aggregation_bits using the state at the attestation's target
   checkpoint, not the head state. The runner now resolves the right
   shuffling via ShufflingCache + regen (mirroring the production
   validation path) instead of `headState.epochCtx.getIndexedAttestation`,
   which only worked when the attestation's epoch happened to be in
   the head's epoch cache (±1 epoch) and broke on cross-epoch fork
   attestations surfaced by the compliance suite.

Adds support for two compliance-only check fields:

- `viable_for_head_roots_and_weights` (consensus-specs#3831): compared
  via `getViableHeads()`. Both sides are sorted by root before
  comparison since the spec doesn't fix order.
- `head_payload_status` (gloas): mapped between our internal enum
  ordering (PENDING=0, EMPTY=1, FULL=2) and spec ordering (EMPTY=0,
  FULL=1, PENDING=2).

Pass rate against the latest comptests workflow `small.tar.gz` artifact:

  fulu/fork_choice_compliance: 253/1472 cases pass (17.2%)

Top remaining failures:
- ~80% `Invalid proposer boost root` — consensus-specs#4807 introduced
  a `block.proposer_index == get_beacon_proposer_index(head_state)`
  guard in `update_proposer_boost_root` that we do not yet implement;
  affects all forks (not just gloas equivocation handling). Tracked
  for follow-up alongside ChainSafe#9233.
- ~1% `Invalid viable heads` — proposer-boost rounding on minimal
  preset (see `getViableHeads()` weight note).
GrapeBaBa added a commit to GrapeBaBa/lodestar that referenced this pull request Jul 7, 2026
Wire the consensus-specs Fork Choice Compliance suite (ChainSafe#3831) into
the existing `forkChoiceTest` runner. The on-disk layout matches the
standard spec-test layout
(`tests/<preset>/<fork>/fork_choice_compliance/<handler>/<suite>/<case>/`),
so it slots in alongside `fork_choice` and `sync` runners.

Three test-only accommodations the compliance fixtures require:

1. `bls_setting: 2` — every compliance fixture uses placeholder
   signatures. Pass `validSignatures: testcase.meta?.bls_setting !==
   BigInt(1)` to `chain.processBlock` so verification short-circuits.
   Standard `fork_choice` fixtures use `bls_setting: 1` so behavior
   there is unchanged.

2. `BLOCK_ERROR_ALREADY_KNOWN` — compliance fixtures intentionally
   re-import the same block (`dup_shift` mutations in their
   `meta.yaml`). Spec semantics for `on_block(store, known_block)` is
   a no-op success. Production block import correctly rejects with
   ALREADY_KNOWN; this runner treats that case as success only when
   the step is `valid: true`.

3. Cross-epoch attestation shuffling — `on_attestation` decodes
   aggregation_bits using the state at the attestation's target
   checkpoint, not the head state. The runner now resolves the right
   shuffling via ShufflingCache + regen (mirroring the production
   validation path) instead of `headState.epochCtx.getIndexedAttestation`,
   which only worked when the attestation's epoch happened to be in
   the head's epoch cache (±1 epoch) and broke on cross-epoch fork
   attestations surfaced by the compliance suite.

Adds support for two compliance-only check fields:

- `viable_for_head_roots_and_weights` (consensus-specs#3831): compared
  via `getViableHeads()`. Both sides are sorted by root before
  comparison since the spec doesn't fix order.
- `head_payload_status` (gloas): mapped between our internal enum
  ordering (PENDING=0, EMPTY=1, FULL=2) and spec ordering (EMPTY=0,
  FULL=1, PENDING=2).

Pass rate against the latest comptests workflow `small.tar.gz` artifact:

  fulu/fork_choice_compliance: 253/1472 cases pass (17.2%)

Top remaining failures:
- ~80% `Invalid proposer boost root` — consensus-specs#4807 introduced
  a `block.proposer_index == get_beacon_proposer_index(head_state)`
  guard in `update_proposer_boost_root` that we do not yet implement;
  affects all forks (not just gloas equivocation handling). Tracked
  for follow-up alongside ChainSafe#9233.
- ~1% `Invalid viable heads` — proposer-boost rounding on minimal
  preset (see `getViableHeads()` weight note).
@nflaig

nflaig commented Jul 8, 2026

Copy link
Copy Markdown
Member

@ensi321 is this still relevant?

ensi321 and others added 4 commits July 8, 2026 20:03
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hardcoding PayloadStatus.PENDING in the weak-parent lookup crashes at
the gloas fork transition: the first gloas block's parent is pre-gloas
(FULL-only), and getNodeIndexByRootAndStatus throws INVALID_NODE_INDEX
when PENDING is requested for a pre-gloas root. The adjacent-parent
guard does not short-circuit this path at the boundary. Use
parentBlock.payloadStatus instead, matching the pattern already used
for the strong-variant lookup elsewhere in this file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ensi321

ensi321 commented Jul 9, 2026

Copy link
Copy Markdown
Member Author

@ensi321 is this still relevant?

For some reason, our unstable is now passing these tests even without this PR and we failed them previously

(name.includes("gloas") &&
(name.includes("simple_attempted_reorg_without_enough_ffg_votes") ||
name.includes("include_votes_another_empty_chain_with_enough_ffg_votes_current_epoch") ||
name.includes("include_votes_another_empty_chain_with_enough_ffg_votes_previous_epoch") ||
name.includes("include_votes_another_empty_chain_without_enough_ffg_votes_current_epoch"))),

But we still need to implement shouldApplyProposerBoost to enforce the correct behaviour.

Added some new tests ethereum/consensus-specs#5441 to make sure this is covered.

@ensi321
ensi321 marked this pull request as ready for review July 9, 2026 14:36
@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 52.49%. Comparing base (e6d38ec) to head (d2bb968).
⚠️ Report is 3 commits behind head on unstable.

Additional details and impacted files
@@            Coverage Diff            @@
##           unstable    #9233   +/-   ##
=========================================
  Coverage     52.49%   52.49%           
=========================================
  Files           848      848           
  Lines         60487    60486    -1     
  Branches       4466     4465    -1     
=========================================
  Hits          31755    31755           
  Misses        28670    28670           
+ Partials         62       61    -1     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d2bb9684f2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/fork-choice/src/forkChoice/forkChoice.ts Outdated

// Indicate whether block arrives in a timely manner ie. before the 4 second mark
// Spec: Store.block_timeliness[ATTESTATION_TIMELINESS_INDEX]
timeliness: boolean;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we may want to use a single block_timeliness: number to represent these 2 flags, cc @wemeetagain to confirm

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a real benefit to this? Using bits impact readability, and we are only saving one boolean field per ProtoNode

@twoeths twoeths left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

implemented is_head_weak() in #9654
need to have it landed before this PR

wemeetagain pushed a commit that referenced this pull request Jul 24, 2026
**Motivation**

- is_head_weak() is needed for should_apply_proposer_boost() #9233
- we also need it for `getProposerHead()`

**Description**
- implement cross-fork
[is_head_weak()](https://github.com/ethereum/consensus-specs/blob/6408b990c0b4e58d4f50ae49b59384e82e5e06d8/specs/gloas/fork-choice.md?plain=1#L739)
spec api and use it in `getProposerHead()`
- track attestation weight in ProtoNode separately, weight still means
total weight
- back propagate boost deltas and attestation deltas separately
- also implement is_parent_strong()


**AI Assistance Disclosure**

- created with the help of Claude

---------

Co-authored-by: Tuyen Nguyen <twoeths@users.noreply.github.com>
Co-authored-by: bing <spiralladder@fastmail.com>
ensi321 and others added 2 commits July 24, 2026 18:08
…roposer-boost

# Conflicts:
#	packages/beacon-node/test/spec/presets/fork_choice.test.ts
…ProposerBoost

is_head_weak() landed via #9654 with the exact gloas semantics (boost-excluded
attestation score + committee-scoped equivocator balance add-back), so the
weak-parent check can call it directly instead of approximating with raw
node.weight against the reorg threshold.

Also adapt findEquivocatingBlocks to the current VariantIndices layout via
getDefaultNodeIndex, and add the new ProtoBlock fields to test fixtures that
were added on unstable since the last merge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ensi321 and others added 2 commits July 24, 2026 21:04
should_apply_proposer_boost judges the parent via is_head_weak against the
attestations known to the store, but the decision ran before applyScoreChanges()
landed this round's deltas, so it read the previous round's attestation scores.
A parent crossing the weak threshold on the pending batch kept the boost
withheld, and a newly detected equivocator kept its discounted vote counted.

For a gloas boosted block, split the score update in two passes: attestation
deltas first, then the boost decision, then the boost deltas. Pre-gloas keeps
the single pass since the boost is unconditional there.

Also replace findEquivocatingBlocks() with hasEquivocatingBlock(): the only
caller tests emptiness, so return on first match instead of allocating a list.

The two ordering tests fail on the previous commit and pass on this one; the
spec vectors cannot catch this because the check step recomputes the head,
self-healing the transient decision before assertions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Measures the two-pass applyScoreChanges overhead against the single-pass
pre-gloas row: 2.96 ms/op vs 2.72 ms/op at vc 600k bc 64 (~9%), paid only
while a gloas block holds proposer boost.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread packages/fork-choice/src/forkChoice/forkChoice.ts
Comment thread packages/fork-choice/src/protoArray/protoArray.ts
Comment thread packages/beacon-node/test/spec/utils/forkChoiceTestRunner.ts Outdated
Comment on lines +593 to +595
this.protoArray.applyScoreChanges({attestationDeltas, proposerBoost: null, ...checkpoints});
const proposerBoost = this.shouldApplyProposerBoost() ? this.getProposerBoost() : null;
this.protoArray.applyScoreChanges({

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is calling applyScoreChanges twice fine? cc @twoeths

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right now applyScoreChanges() does 2 things:

  • (1) update node's weights
  • (2) update best child/descendants

we can improve by calling 1) twice and call 2) in the end
it's more like a refactor and does not affect fulu so we can just leave a TODO and address in another PR I guess

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added TODO comment

Comment thread packages/fork-choice/src/forkChoice/forkChoice.ts Outdated
@ensi321

ensi321 commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

Going to reflect the changes from ethereum/consensus-specs#5515 also

ensi321 added a commit that referenced this pull request Aug 26, 2026
The alpha.14 comptests were regenerated, so the seed-embedding case names
changed again. Full re-run and re-triage of the gloas suite:

- #9821 (exact Gwei weights) fixed the proposer boost weight quantization
  divergence (#9694); that skip bucket is removed.
- #9864 (recompute head after pulling up checkpoints) fixed the stale
  cached head read in the proposer boost dependent-root gate (#9666);
  both remaining pins are removed.
- 127 cases remain skipped for consensus-specs#5496 (childless
  payload-status variants bypass the filter_block_tree FFG check),
  regenerated names. This is the only remaining failure bucket.

Suite result with #9233 merged locally: 1345 passed, 0 failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Vw1BUX36pwcv52fGr3n1C

@nflaig nflaig left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@nflaig
nflaig merged commit 2fba076 into unstable Aug 27, 2026
23 of 24 checks passed
@nflaig
nflaig deleted the nc/should-apply-proposer-boost branch August 27, 2026 09:27
nflaig added a commit that referenced this pull request Aug 31, 2026
Implement the proposer equivocation branch of `get_proposer_head` from
ethereum/consensus-specs#4807, which reorgs a weak previous-slot head
whose proposer is known to have equivocated:

```python
elif all([head_weak, current_time_ok, proposer_equivocation]):
    return parent_node
```

This is the proposer side of the builder reveal safety argument that
#9233 implemented the attester side of. A builder reveals once it sees a
block with enough weight and no equivocation. If the proposer then
publishes an equivocating block and the next proposer extends it,
attesters withhold the boost via `should_apply_proposer_boost`, but for
the equivocating block to actually be reorged the next proposer has to
build on the parent, which is this branch. It lives in the phase0 spec
so it applies pre-gloas as well.

- `getProposerHead` returns the parent when the head is weak, from the
previous slot and another block at the same slot from the same proposer
is in fork choice, skipping the regular reorg conditions (`head_late`,
`ffg_competitive`, `finalization_ok`, `proposing_on_time`,
`parent_strong`). The boost worn off check stays a precondition for both
branches, the spec asserts it before either
- `shouldOverrideForkChoiceUpdate` mirrors the branch so the fcu skip at
import and the payload attributes prediction in `prepareNextSlot` follow
`getProposerHead`. It does not check `is_head_weak`, the head slot's
attestations are still queued when it runs so the head is assumed weak,
same as the regular branch
- `hasEquivocatingBlock` takes a `ptcTimelyOnly` flag,
`should_apply_proposer_boost` only counts PTC-timely siblings while
`is_proposer_equivocation` counts any known block
- the gloas parent payload status is preserved, `parentBlock` is already
resolved via `getParentPayloadStatus`
- link the `is_proposer_equivocation`, `should_apply_proposer_boost` and
`record_block_timeliness` specrefs to their implementations and bump the
`get_proposer_head` spec link, `v1.4.0-beta.4` predates this branch

The existing check order and `notReorgedReason` values are unchanged,
the new branch only runs ahead of them. Equivocating blocks reach fork
choice since #9805, before that this could only trigger for blocks
fetched via unknown block sync.

There are no spec vectors for this branch upstream, coverage is via the
`getProposerHead` and `shouldOverrideForkChoiceUpdate` unit tables. The
four reorg cases fail on `unstable` and pass here, and the
`get_proposer_head`, `reorg`, `should_override`, `ex_ante` and
`should_apply_proposer_boost` fork choice spec tests still pass.

Closes #9764
@wemeetagain

Copy link
Copy Markdown
Member

🎉 This PR is included in v1.47.0 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

6 participants